using System;
using System.Collections.Generic;
using System.Linq;
using GearSynth.Evaluation;
using GraphSynth.Representation;
using OptimizationToolbox;
using StarMathLib;


namespace GearSynth
{
    internal class ParametricTuning : abstractOptMethod
    {
        private const int NumStartPoints = 20;
        Random rnd;
        readonly userDefinedGoals udg;
        readonly List<GearFamily> GearLibrary;
        List<Role> gearRoles;
        int numGears;
        GearFamily[] gearFamilies;
        DesignSpaceDescription gearSpace;
        long[] CurrentIndices;
        private readonly string outputDirectory;
        private string p;

        // constructor
        internal ParametricTuning(userDefinedGoals udg, string outputDirectory, List<GearFamily> GearLibrary)
        {
            rnd = new Random();
            this.udg = udg;
            this.outputDirectory = outputDirectory;
            this.GearLibrary = GearLibrary;
            Add(new squaredExteriorPenalty(this, 10.0));
            ConstraintsSolvedWithPenalties = false;
        }

        internal Boolean TuneCandidate(candidate current)
        {
            gearRoles = FindRolesAndReorder(current);
            numGears = gearRoles.Count;
            gearFamilies = FindGearFamilies(current, numGears);

            /* here is the Dependent Analysis. Take a look at the file/class ForceVelocityPositionAnalysis.cs
             * and notice that it inherits from IDependent Analysis. By adding this to the optimization method 
             * (line 122), we are ensuring that it is called for any new decision variables found in the process.*/
            var GTAnalysis = new GearTrainAnalysis(gearRoles, udg, numGears, gearFamilies);
            Add(GTAnalysis); // this tell the optMethod method functions that this is the DependentAnalysis.
            /* here is the objective function, minimize mass. Note that it will hold a reference to the 
             * ForceVelocityPositionAnalysis so that it can reference it for exact values of diamter. */
            var minMass = new massObjective(GTAnalysis);
            Add(minMass);
            //setup constraints for optimization
            //slot 1 - number of teeth
            //slot 2 - pitch
            //slot 3 - face Width
            //slot 4 - location variable
            /* here is an inequality constraint for fitting within the box described above. Again, it
             * needs to position and diameter information stored in ForceVelocityPositionAnalysis */
            var bbConstr = new boundingboxConstraint(GTAnalysis, udg);
            Add(bbConstr);
            /* on and on: stress inequality, output Location, output Speed equalities. Details can be found in 
             * http://dx.doi.org/10.1115/DETC2009-86780 */
            var stressConstraint = new stressConstraint(GTAnalysis, udg);
            Add(stressConstraint);
            var locationConstraint = new outputLocationConstraint(GTAnalysis, udg);
            Add(locationConstraint);
            var speedConstraint = new outputSpeedConstraint(GTAnalysis, udg);
            Add(speedConstraint);
            var pitchConstraints = new List<samePitch>();
            for (var i = 1; i < numGears; i++)
                if (!gearRoles[i].Equals(Role.driving))
                {
                    // each mating gear pair must have the same pitch. 
                    var newPitchConstraint = new samePitch(i * 4 + 1, (i - 1) * 4 + 1);
                    pitchConstraints.Add(newPitchConstraint);
                    Add(newPitchConstraint);
                }

            gearSpace = new DesignSpaceDescription();
            foreach (var family in gearFamilies)
                gearSpace.Add(new VariableDescriptor(0, family.gears.GetLength(0),
                    family.gears.GetLength(0)));


            CurrentIndices = new long[numGears];
            var xInit = new double[4 * numGears];
            var xStarOuter = new double[4 * numGears];
            fStar = double.PositiveInfinity;
            Boolean foundFeasibleGearSet = false;
            do
            {
                var gearIndices = gearSpace.GetVariableVector(CurrentIndices);
                for (int i = 0; i < numGears; i++)
                {
                    var gearData = StarMath.GetRow((int)gearIndices[i], gearFamilies[i].gears);
                    xInit[4 * i] = gearData[0];
                    xInit[4 * i + 1] = gearData[1];
                    xInit[4 * i + 2] = gearData[2];
                    xInit[4 * i + 3] = 0.0; //location is determined in subsequent optimization
                    //if other constraints are found to be feasible.
                }
                if (pitchConstraints.All(p => feasible(p, xInit))
                    && feasible(speedConstraint, xInit)
                    && feasible(stressConstraint, xInit))
                {
                    SearchIO.output("** Found one that meets speed, and stress! now optimizing location.", 3);
                    foundFeasibleGearSet = true;
                    var paramOptimization = SetUpLocationOptimization();
                    fixGearDataInDecisionVector(paramOptimization, xInit);
                    paramOptimization.Add(new fixedMassObjective(calc_f(xInit, false)));
                    paramOptimization.Add(locationConstraint);
                    paramOptimization.Add(bbConstr);
                    paramOptimization.Add(GTAnalysis);
                    double fNew;
                    double[] xStarInner;
                    int iteration = NumStartPoints;
                    do
                    {
                        AssignRandomLocationVars(xInit);
                        fNew = paramOptimization.Run(out xStarInner, xInit);
                    } while (iteration-- > 0 && paramOptimization.feasible(locationConstraint, xStarInner)
                             && paramOptimization.feasible(bbConstr, xStarInner));
                    if (fNew < fStar)
                    {
                        fStar = fNew;
                        xStarOuter = xStarInner;
                    }
                }
            } while (IncrementIndices(numGears));

            if (!foundFeasibleGearSet) xStarOuter = xInit;
            current.designParameters = new List<double>(xStarOuter);
            current.f0 = calc_f(xStarOuter,false);
            current.f1 = calcInefficiency();
            current.f2 = calc_f(xStarOuter, true)-current.f0;
            return (current.f2 <= 0);
        }

        private void AssignRandomLocationVars(double[] x)
        {
            for (int i = 3; i < x.GetLength(0); i = i + 4)
                x[i] = 2 * Math.PI * rnd.NextDouble();
            /* a random value between 0 and 360 (2pi) which makes sense
             * when it's an angle but not necessarily a length. Although, 
             * in both inches and mm it's not bad (and should be positive anyway). */
        }


        private void fixGearDataInDecisionVector(abstractOptMethod optMethod, double[] xInit)
        {
            var fixedGears = new DesignSpaceDescription();
            for (int i = 0; i < xInit.GetLength(0); i++)
                if (i % 4 == 3) fixedGears.Add(new VariableDescriptor());
                else fixedGears.Add(new VariableDescriptor(new[] { xInit[i] }));

            optMethod.Add(fixedGears);
        }

        private abstractOptMethod SetUpLocationOptimization()
        {
            #region Set up optMethod

            //NelderMead optMethod =
            //    new NelderMead(.001, 10, true);
            //GradientBasedUnconstrained optMethod =
            //    new GradientBasedUnconstrained(10);
            var optMethod = new GradientBasedOptimization();
            //SequentialQuadraticProgramming optMethod = new SequentialQuadraticProgramming(true);
            //GeneralizedReducedGradientActiveSet optMethod =
            //    new GeneralizedReducedGradientActiveSet(true);
            optMethod.Add(new ArithmeticMean(0.001, 2, 200));
            //optMethod.Add(new GoldenSection(optMethod, 0.001,200, int.MaxValue));
            //optMethod.Add(new BFGSDirection());
            optMethod.Add(new FletcherReevesDirection());
            optMethod.Add(new MaxIterationsConvergence(200));
            optMethod.Add(new DeltaFConvergence(0.0001, 0.000001));
            //optMethod.Add(new convergenceBasic(BasicConvergenceTypes.AndBetweenSetConditions, 20, 0.01, double.NaN, double.NaN, int.MaxValue));
            optMethod.Add(new squaredExteriorPenalty(optMethod, 10.0));
            //optMethod.Add(new linearExteriorPenaltySum(optMethod, 10.0));
            optMethod.Add(new ConvergeIfFeasible(optMethod));
            return optMethod;

            #endregion
        }

        private Boolean IncrementIndices(int limit, int IndicesIndex = 0)
        {
            if (IndicesIndex == limit) return false;
            CurrentIndices[IndicesIndex]++;
            if (CurrentIndices[IndicesIndex] >= gearSpace.MaxVariableSizes[IndicesIndex])
            {
                CurrentIndices[IndicesIndex] = 0;
                return IncrementIndices(limit, IndicesIndex + 1);
            }
            return true;
        }



        private GearFamily[] FindGearFamilies(candidate current, int numGears)
        {
            var result = new GearFamily[numGears];
            for (int i = 0; i < numGears; i++)
                result[i] = GearLibrary.Find(family => current.graph.nodes[i].localLabels.Contains(family.label));
            return result;
        }


        private static List<Role> FindRolesAndReorder(candidate c)
        {
            /* find first gear (connected to input shaft) */
            node inputShaft = (from n in c.graph.nodes
                               where (n.localLabels.Contains("seed") && n.localLabels.Contains("shaft"))
                               select n).First();
            node inputGear = (from a in inputShaft.arcsFrom
                              where (a.otherNode(inputShaft).localLabels.Contains("gear"))
                              select a.otherNode(inputShaft)).First();

            c.graph.nodes.Remove(inputGear);
            c.graph.nodes.Insert(0, inputGear);

            int gearcount = 0;
            Boolean foundNextGear;
            var roles = new List<Role> { Role.driving }; //the input gear is always a driving gear.
            do
            {
                foundNextGear = false;
                node lastGear = c.graph.nodes[gearcount++];
                node gear = (from a in lastGear.arcsFrom
                             where (a.otherNode(lastGear).localLabels.Contains("gear"))
                             select a.otherNode(lastGear)).FirstOrDefault();
                if (gear != null)
                {
                    c.graph.nodes.Remove(gear);
                    c.graph.nodes.Insert(gearcount, gear);
                    if (gear.localLabels.Contains("driven")) roles.Add(Role.driven);
                    else if (gear.localLabels.Contains("idler")) roles.Add(Role.idler);
                    else //roles.Add(Role.driving); 
                        //most likely this will not be a driving gear (impossible?...even for MIMO? I think so. Maybe this should throw an exception.
                        // yes, let's throw an exception.
                        throw new Exception(
                            "It is impossible for a gear in this context to be a driving gear (probably is idler) please check rules.");
                    foundNextGear = true;
                }
                else  // this means that either that was the last gear or we need to traverse thru an idler shaft
                {
                    node shaft = (from a in lastGear.arcsFrom
                                  where (a.otherNode(lastGear).localLabels.Contains("shaft"))
                                  select a.otherNode(lastGear)).FirstOrDefault();
                    if (shaft != null)
                    {
                        gear = (from a in shaft.arcsFrom
                                where (a.otherNode(shaft).localLabels.Contains("gear"))
                                select a.otherNode(shaft)).FirstOrDefault();
                        if (gear != null)
                        {
                            c.graph.nodes.Remove(gear);
                            c.graph.nodes.Insert(gearcount, gear);
                            if (gear.localLabels.Contains("driving")) roles.Add(Role.driving);
                            else if (gear.localLabels.Contains("idler")) roles.Add(Role.idler);
                            else //roles.Add(Role.driven);
                                //Like the above case - it is impossible for this to be a driven gear.
                                throw new Exception(
                                    "It is impossible for a gear in this context to be a driven gear (probably is idler) please check rules.");
                            foundNextGear = true;
                        }
                    }
                }
            } while (foundNextGear);
            return roles;
        }


        private double calcInefficiency()
        {
            double totEff = 1.0;
            for (int i = 0; i < gearRoles.Count; i++)
            {
                if (!gearRoles[i].Equals(Role.driving))
                    switch (gearFamilies[i].gearType)
                    {
                        case GearType.spur: totEff *= 0.98; break;
                        case GearType.helical: totEff *= 0.95; break;
                        case GearType.bevel: totEff *= 0.9; break;
                        case GearType.wormgear: totEff *= 0.7; break;
                    }
            }
            return 100.0 * (1 - totEff);
        }

        #region Overrides of abstractOptMethod

        /// <summary>
        /// Runs the specified optimization method. This includes the details
        ///             of the optimization method.
        /// </summary>
        /// <param name="xStar">The x star.</param>
        /// <returns/>
        protected override double run(out double[] xStar)
        {
            throw new NotImplementedException();
            // indeed this class is not really an optimization method called from 
            // within OOOT, but we are interested in using many of the aspects of
            // the abstractOptMethod in OOOT. Specifically, the storage and calc-
            // ulation of f, g, and h.
        }

        #endregion
    }
}

